Skip to content

fix: Claude sandbox followups#340

Closed
gilesknap wants to merge 20 commits intoDiamondLightSource:claude-sandboxfrom
gilesknap:claude-sandbox-followups
Closed

fix: Claude sandbox followups#340
gilesknap wants to merge 20 commits intoDiamondLightSource:claude-sandboxfrom
gilesknap:claude-sandbox-followups

Conversation

@gilesknap
Copy link
Copy Markdown
Contributor

@gilesknap gilesknap commented Apr 29, 2026

6 follow-ups against the merged claude-sandbox branch:

  • Re-blank VS Code askpass vars at just claude invocation. remoteEnv blanking only sets the remote server's initial environment; VS Code's Git extension re-injects GIT_ASKPASS and VSCODE_GIT_IPC_HANDLE per child-process spawn, defeating the sandbox. The just claude recipe now re-blanks both vars on the command line. README-CLAUDE.md updated to explain the mechanism.
  • TODO comments for node/gh/glab → base image. Per PR Add claude sandbox #337 review: these will move into ubuntu-devcontainer once it ships on Ubuntu 26.04. Comments added at each install block.
  • Drop forwardPorts: [8000] / remote.autoForwardPorts: false. Per PR Add claude sandbox #337 review: sphinx-autobuild-specific config that shouldn't be a default. Drift test still passes.
  • Always render postCreate.sh. Previously gated on add_claude; the no-claude case used a long inline bash string. Now always rendered with Claude-specific bits wrapped in {% if add_claude %}. Per PR Add claude sandbox #337 review.
  • Only init missing submodules on container start. Old behaviour silently moved already-initialised submodules into detached HEAD on rebuild. Now parses git submodule status and only initialises uninitialised entries. Applies to all generated projects.
  • Drop install_gh / install_glab copier questions. gh and glab are now installed unconditionally when add_claude is on, in anticipation of them moving into the base image. Removes the matching jinja gating from Dockerfile, devcontainer.json, postStart.sh and justfile.

gilesknap and others added 5 commits April 29, 2026 18:52
VS Code's built-in Git extension re-injects GIT_ASKPASS and
VSCODE_GIT_IPC_HANDLE per child-process spawn, overriding the
remoteEnv blanking in devcontainer.json. Re-blank both on the
`just claude` command line so claude cannot reach VS Code's
credential bridge. README-CLAUDE.md updated to document why.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per PR DiamondLightSource#337 review: nodejs, gh, and glab are slated to move into the
ubuntu-devcontainer base image once it ships on Ubuntu 26.04, where
all three are available from apt at recent enough versions. Add TODO
comments at each install block so the cleanup is obvious when 26.04
lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VS Code's default port auto-detection forwards the port when the user
clicks the link in sphinx-autobuild output, so the explicit
forwardPorts: [8000] and remote.autoForwardPorts: false override
weren't pulling their weight as defaults. Per PR DiamondLightSource#337 review: this
was sphinx-autobuild-specific config that crept in and shouldn't ship
with every sphinx project.

Removed from both the meta repo's own devcontainer.json (the
add_claude=no baseline) and the templated copy, keeping the drift
test happy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously postCreate.sh only existed when add_claude=yes; the
add_claude=no case used a long inline bash string in
postCreateCommand. Tidier to always have a script and gate the
Claude-specific bits with Jinja {% if add_claude %} inside it, per
PR DiamondLightSource#337 review.

The meta repo now has its own .devcontainer/postCreate.sh that
mirrors the add_claude=no rendering, and its devcontainer.json
calls it the same way. Drift test still passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
git submodule update --init runs on every postCreate, which on
container rebuild moves any submodule whose HEAD differs from the
parent's pinned SHA into detached HEAD — silently yanking
in-progress branch work in initialized submodules. The data isn't
lost (reachable via reflog and existing branches) but recovery
requires submodule literacy that not every user has.

Filter to only the uninitialized submodules using git submodule
status output (leading '-'), preserving the first-clone build
protection without touching already-checked-out submodules. Applies
to all generated projects, not just the Claude-sandbox case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gilesknap gilesknap requested a review from coretl April 29, 2026 18:55
@gilesknap
Copy link
Copy Markdown
Contributor Author

@coretl followup to your comments on #337

Prompt:
See #337. regarding the inclusion of glab and gh in the base image, we need to wait for an update to the base image to ubuntu 26.04 so just add comments to the effect that this will change. Make one commit for that and one commit for each of the other points in the PR comments - discuss each one with me before making changes. go.

@gilesknap gilesknap changed the title Claude sandbox followups fix: Claude sandbox followups Apr 29, 2026
gilesknap and others added 5 commits April 29, 2026 19:12
Reinstate the README → README-CLAUDE.md table row removed in the
previous commit, using {{repo_url}}/blob/main/... so the link works in
both renderers:

- myst (Sphinx) treats it as an external URL and skips xref resolution,
  so --fail-on-warning no longer trips on myst.xref_missing.
- GitHub renders it as a normal link to the file in the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous absolute URL pointed at the example repo's main branch,
which won't have README-CLAUDE.md until the example is regenerated
with add_claude=yes — chicken-and-egg, so linkcheck 404s.

Raw <a href="./README-CLAUDE.md"> works in both renderers without that
dependency:
- myst: passes through as inline HTML, no xref_missing warning.
- sphinx linkcheck: doesn't follow raw HTML hrefs (verified locally),
  so no 404.
- GitHub: renders as a normal clickable link to the file in the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Always install gh and glab when add_claude is on. The base image will
ship them on Ubuntu 26.04, at which point the Dockerfile install blocks
can be dropped wholesale; in the meantime, having them unconditional
removes two questions and the matching jinja gating across the
Dockerfile, devcontainer.json, postStart.sh and justfile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If the user opens the devcontainer before running `git init`, uv sync
fails opaquely because setuptools-scm can't compute a version, and
pre-commit can't install hooks into a non-existent .git/hooks. VS
Code's wrapper then hides the real stderr behind a generic
"postCreateCommand failed" exit-1.

postCreate.sh now detects a missing .git directory up front, prints
a clear explanation to stderr, and exits 1. The user is told exactly
what to run on the host to recover. Failing fast beats silently
bootstrapping a half-broken environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gilesknap gilesknap force-pushed the claude-sandbox-followups branch from 6c08e63 to 19e9846 Compare April 29, 2026 20:03
@gilesknap
Copy link
Copy Markdown
Contributor Author

gilesknap commented Apr 29, 2026

❯ how does the sandboxing look?

...

● Sandbox looks intact. Here's the picture:

Defense layers (all enforced):

  • .claude/settings.json:7 allows Bash(*) but the .claude/hooks/sandbox-check.sh UserPromptSubmit hook gates every prompt — if any leak indicator is set, the prompt is blocked with exit 2.
  • devcontainer.json:12-25 blanks SSH_AUTH_SOCK, GIT_ASKPASS, VSCODE_GIT_IPC_HANDLE, VSCODE_GIT_ASKPASS_* in remoteEnv, and sets IN_DEVCONTAINER=1.
  • postStart.sh (also wired to postAttachCommand at devcontainer.json:109) wipes credential.helper from both --system and --global scopes, deletes the /tmp/vscode-remote-containers-*.js bridge, and pins gh/glab helpers to the in-container binaries.
  • .claude/settings.json:9-21 denies destructive/exfil bash families (git push --force, git reset --hard, ssh*, scp, rsync, sftp, mail, sendmail).

Live verification (just ran from this shell):

  • IN_DEVCONTAINER=1 ✓
  • SSH_AUTH_SOCK, GIT_ASKPASS, VSCODE_GIT_IPC_HANDLE all unset ✓
  • /root/.ssh has only known_hosts ✓
  • No /tmp/vscode-remote-containers-*.js bridge ✓
  • --system credential.helper unset ✓
  • Global helpers point only at in-container gh/glab ✓
  • git credential fill for github.com returns no PAT (fails with "No such device") ✓

One small drift worth flagging: the glab helper is pinned to /usr/local/bin/glab while gh resolved to /usr/bin/gh. That's expected — postStart.sh:46 uses command -v glab at runtime. Just noting it because the README example at README-CLAUDE.md:38 describes rewriting away from /usr/local/bin/gh, and here the situation is mirrored for glab. Not a leak, just a path quirk.

Deliberately exposed (per README-CLAUDE.md): ~/.claude bind-mount, --net=host, /workspaces parent mount, shared /cache volume.

All documented as intentional trade-offs.

Net: sandbox is healthy and the runtime state matches the policy.

❯ are the intentional trade offs acceptable?

● Mostly yes, with two worth thinking about harder. Quick read on each:

~/.claude bind-mount — biggest hidden surface area. With --dangerously-skip-permissions, anything Claude writes to ~/.claude/hooks/ or ~/.claude/skills/ runs on every future Claude session in every project, on the host. The README does flag this ("treat ~/.claude as part of the sandbox boundary"), but it means a single compromised prompt in this project can persist into unrelated work. Acceptable if you trust Claude not to scribble in its own config, less so as a defense-in-depth boundary. Worth periodically auditing ~/.claude/hooks/ and ~/.claude/skills/ for anything unexpected.

--net=host — broad, but the justification is concrete (X11 + EPICS CA for Diamond instrument control + no devcontainer port-forwarding). Practical implication: anything bound to localhost on the host (databases, dev servers, local model endpoints, secrets daemons) is reachable from inside. If you don't run sensitive local services on the host while Claude is working, it's fine. Bridge networking would be safer but materially less useful for the use case.

/workspaces parent mount — fine as long as you don't park unrelated/sensitive repos in the same parent dir. It's a habit thing, not a sandbox flaw.

/cache shared volume — uv cache poisoning is the realistic risk, but bounded to template-derived containers and gated by uv's own hash verification on locked deps. Acceptable.

Net: acceptable for the intended workflow. The one I'd actually do something about is ~/.claude — if you want a tighter boundary, you could split per-project via a wrapper that points --settings-dir at a project-local copy, at the cost of losing cross-project memory/skills.

@gilesknap
Copy link
Copy Markdown
Contributor Author

@coretl see above - I investigated partial RO mount of .claude but it gets messy. We don't have to bind mount .claude but it looses lots of cross project memory features and common config.

gilesknap and others added 10 commits April 30, 2026 05:01
…derived skill triggers

Three sandbox guards (SSH_AUTH_SOCK, VSCODE_GIT_IPC_HANDLE, GIT_ASKPASS) match
exactly what `just claude` clears, so the failure messages now name that
recipe. The other guards keep their original remediations because `just
claude` would not fix them.

Also extend the copier-derived skill description to fire on .claude/,
CLAUDE.md, and README-CLAUDE.md edits — these are template-managed but
weren't listed as triggers, so the skill was being missed for sandbox edits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tings

Pin three Dev Containers settings in devcontainer.json so VS Code never
forwards host git credentials into the container in the first place,
rather than relying on env-var clearing in `just claude`:

  git.terminalAuthentication: false              -> stops VS Code's Git
    extension from setting GIT_ASKPASS / VSCODE_GIT_IPC_HANDLE in
    integrated terminals (source: vscode/extensions/git/src/askpass.ts)
  dev.containers.gitCredentialHelperConfigLocation: "none"
                                                 -> stops Dev Containers
    from writing a credential.helper line into /etc/gitconfig
  dev.containers.copyGitConfig: false            -> stops the host
    ~/.gitconfig (with its url.insteadOf rewrites and per-host helpers)
    from being copied into the container

A compromised model can no longer reach a leaked VS Code IPC socket by
editing the hook or recipe, because the socket is never wired up.

Knock-on cleanup:
- devcontainer.json: drop the now-redundant remoteEnv blanks for
  GIT_ASKPASS / VSCODE_GIT_IPC_HANDLE / VSCODE_GIT_ASKPASS_{MAIN,NODE,
  EXTRA_ARGS}. The new settings prevent VS Code from setting them at
  all; sandbox-check.sh catches any regression. SSH_AUTH_SOCK= stays
  in remoteEnv — there is no VS Code setting alternative for it.
- justfile: drop VSCODE_GIT_IPC_HANDLE= / GIT_ASKPASS= from `just claude`
  for the same reason; SSH_AUTH_SOCK= stays.
- sandbox-check.sh: env-var failures for the credential bridge now
  point at "rebuild the devcontainer" since `just claude` no longer
  fixes them.
- README-CLAUDE.md: rewrite the credential-injection section to lead
  with the settings as the primary defence; postStart.sh and the
  sandbox-check.sh hook are belt-and-braces verifications, not the
  only enforcement.

Requires devcontainer rebuild for the settings to take effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Block prompt submission when a VS Code reconnect drops either gitconfig
bind: /root/.gitconfig grep'd for vscode-remote-containers credential
bridges, /etc/gitconfig checked via `git config --system --list` so any
leaked system-scope setting (core.hooksPath, url.insteadof, http.proxy,
credential.helper) trips the hook, not just credential helpers.

verify-sandbox skill: replace the narrow 6c (system credential.helper
only) with 6c (/etc/gitconfig masked) + 6d (system scope is empty).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Jinja's default settings keep the newline after `{% endif %}`, so the
rendered postCreate.sh ended with `bash\n\n` (a trailing blank line).
end-of-file-fixer in pre-commit flagged this, breaking lint/run and
all `test_template_*` jobs that exercise the generated project.

`{% endif -%}` strips the trailing newline; removing the blank source
line above `{% if %}` keeps the spacing tidy when add_claude is false.
Also normalises the meta repo's own .devcontainer/postCreate.sh to
match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gilesknap gilesknap closed this Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant